Skip to content

Set default CONFIG_BROADCAST_TABLE_SIZE - #728

Open
MattWestb wants to merge 2 commits into
zigpy:devfrom
MattWestb:patch-1
Open

Set default CONFIG_BROADCAST_TABLE_SIZE#728
MattWestb wants to merge 2 commits into
zigpy:devfrom
MattWestb:patch-1

Conversation

@MattWestb

@MattWestb MattWestb commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Zigbee PRO must have it set to 15 and if user like doing it different they can setting it in config on there own risk braking broadcast in the mesh network (if using star network its not a problem)

Zigbee PRO must have it set to 15 and if user like doing it different they can setting it in config
@MattWestb

MattWestb commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Sorry @puddly for pinging you in the morning !!!
I have putting in default for broadcast table but one test is not likeing it but i think its unrelated to my change.

This is the right way if like hotting the broadcast with "extended firmware" it shall being done in config like:

    ezsp_config:
      CONFIG_MAX_END_DEVICE_CHILDREN: 0
      CONFIG_BROADCAST_TABLE_SIZE: 254

I running my IKEA controller test network with the config and its with the stock ZBT-2 firmware.
My production system is running on Billy EZSP and have Silabs standard so shall being 15 in the firmware.

@puddly

puddly commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@MattWestb Given zigpy/ziggurat#43 and zigpy/ziggurat#55, do you still think SiLabs default of 15 is right? Nobody really uses such a conservative config even for manufacturers using SiLabs chips, no?

@MattWestb

MattWestb commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Its up to you but its depends of the users network if all routers is "extended" is it OK.
I think the best / safe way for not getting problem is making it default 15 and have instruction in the wiki for overriding it (if the firmware is supporting it) then users is (hopefully) understand what was going wrong with there adaptive lighting in broadcast mode.
My opinion is ZHA shall being default "safe configured" out of the box and if like tuning it then the user is doing it on own risk and not the opposite (Z2M is allowing have joining open 24/7 and most users is not knowing it and what it can happening).

Feel free do what you like / feel is the right for ZHA in the end = fixing or closing.

As normal great work / digging done !!

@TheJulianJES
TheJulianJES self-requested a review August 5, 2026 16:55
@TheJulianJES

TheJulianJES commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

While I totally understand the concern behind the varying broadcast limits, I think the community feedback so far was that the higher broadcast table size actually helped them.

In most cases, I think it's only really relevant for users manually dragging the color wheel slider in the HA UI. And there, you either get an error message really fast with the 15 limit or everything works for much longer with the higher limits. In the worst case, users may see some (further away) routers not respond with the higher limit, but with the increased broadcast limits that many manufacturers (of routers) seem to use with SiLabs, I think that's not a huge concern.

Should this turn out to be an actual issue, we could even add a settings toggle in the frontend which is like "Limit broadcast messages" with an explanation to use that when you have a busy network or congested airtime, perhaps (to reserve "airtime" for unicasts, even if a lot of broadcast commands are being transmitted).

Grouped Hue lights testing

I do have 11 Hue lights grouped in a garden I've just tested with and even spamming broadcast commands (dragging color wheel sends commands every 500 ms), everything seems to react just fine there. They're all not "directly connected" to the coordinator, most have Hue lights in between.

Only when spamming them really hard after some time (rapidly clicking color wheel as fast as possible, so broadcast commands get sent every few millisecond) do I see sl_Status.ZIGBEE_MAX_MESSAGE_LIMIT_REACHED errors (which are actually NETWORK_BUSY but the mapping for older Gecko SDK-based firmwares currently changes it to ZIGBEE_MAX_MESSAGE_LIMIT_REACHED displayed in the frontend's error message, which is a bug I'll put up a PR for).

So at that point, I do manage to hit the broadcast table size limit again. It also seems like some lights rarely do desynchronize when I really spam it. But I guess that's because the 64 limit is higher than the one Hue is using*. IMO, it's not a big issue though, as you have to hit it really hard to cause that. I think this is acceptable.

(*: Limit for outgoing broadcasts would be 58 – the bot comment will have more on this. But even that number is likely still slightly higher than the Hue limit, though again, I don't think it's a big issue currently.)

Right balance

Overall, I think this boils down to figuring out a good balance. That manufacturers already use higher broadcast limits than the default 15, even though it is warned against, is because the experience with Zigbee (light) groups is just somewhat bad otherwise. So I think we should also keep the higher limit by default to have a comparable experience to using the Hue/IKEA Bridge with light groups directly, for example.

On this PR

I'll have the bot write something as well here to see if there's extra information but I think the PR is not doing what you want it to do at the moment. Currently, this PR would always set a minimum of 15 for the broadcast table size. This is already enforced by the SiLabs SDK at compile time IIRC. If a firmware is compiled with a higher value, this PR would not change that.

@zigpy-review-bot

Copy link
Copy Markdown
Collaborator

Some supporting detail for the points above — what the firmware on the common coordinators actually ships, and the numbers behind the "error quickly at 15, works much longer at higher values" difference.

What the change does on real hardware

minimum=True means bellows only ever raises the NCP's value, never lowers it, and the apply logic skips the write entirely when the current value is already at or above the target. EmberZNet's stock default is 15, and 15 is also its compile-time floor, so on stock firmware the write is always skipped.

On the mainstream Home Assistant coordinators the gap is much wider, because their firmware does not use the SiLabs default at all. From the silabs-firmware-builder manifests at the current release, v2026.02.23:

Coordinator Broadcast table size
Connect ZBT-2 64
SkyConnect 64
Yellow 64

This is not a recent change — the value is already 64 at v2026.01.11-beta2 and is still 64 on main today. One thing that makes it easy to miss when grepping: the setting is spelled EMBER_BROADCAST_TABLE_SIZE in the released coordinator manifests and was renamed to SL_ZIGBEE_BROADCAST_TABLE_SIZE on main with the Simplicity SDK 2026.6.0 update, so it is worth searching for both names.

So on a ZBT-2 this PR changes nothing: the value is already 64 and the write is skipped. Reassuringly, it also cannot clamp that 64 back down — minimum=True only raises.

One edge case where it is not a no-op

If getConfigurationValue returns a non-OK status, the minimum check short-circuits and the code proceeds to write 15. On firmware where the read is unsupported but the write is not, that would shrink the table. This shape is not specific to this PR — every existing minimum=True entry shares it — but it is worth a guard if broadcast table size becomes one of them.

What the table size controls on a coordinator

Origination capacity is roughly the table size minus six — about 58 on a ZBT-2 at 64, about 9 if it were 15

Since a ZBT-2 running bellows is only ever a coordinator, the operative day-to-day pressure is not how much it relays for others but how much it can originate. That is governed by CONFIG_NEW_BROADCAST_ENTRY_THRESHOLD, and the important detail is that the threshold is derived from the table size rather than independent of it. In routing/util/broadcast.h:

#define BROADCAST_TABLE_SIZE sli_zigbee_broadcast_table_size
...
#define NEW_ENTRY_THRESHOLD (BROADCAST_TABLE_SIZE - 6)

BROADCAST_TABLE_SIZE there is the runtime variable, so the threshold is defined six below the table size. Origination capacity is therefore roughly the table size minus six: about 58 on a ZBT-2 at 64, and about 9 if it were 15.

That makes the table size the value governing how many broadcasts the host can send before the stack refuses to originate any more — SiLabs' own wording for the threshold is that "the local device will fail to originate a broadcast message after this threshold is reached". It is very likely why these coordinators ship 64.

Those two figures are the difference described above, quantified: roughly 58 commands before a refusal at 64, against roughly 9 at 15. On current Simplicity SDK firmware the refusal arrives as a generic sl_Status.BUSY, which is the status the legacy mapping noted above should resolve to.

A "limit broadcast messages" option, if one were ever wanted, would sit naturally on this threshold rather than on the table size. CONFIG_NEW_BROADCAST_ENTRY_THRESHOLD is separately settable over EZSP, and lowering it caps what the coordinator originates while leaving the rest of the table available for relaying other devices' broadcasts — much closer to "reserve airtime for unicasts" than shrinking the table, which would cut relay capacity too.

Why the safe default is the larger value, not the smaller one

The principle raised for this PR — that ZHA should be safe out of the box, and tuning should be opt-in — seems right to me. I think it argues for leaving these coordinators at 64.

Consider what a user actually does to trigger this: dragging the colour wheel for a Zigbee group in the UI. The frontend throttles those updates to one command every 500 ms while the pointer is moving, so it is a sustained couple of commands per second for as long as the drag lasts — user-driven rather than steady state, and the scenario zigpy/ziggurat#43 is about. There are two ways it can go.

With a small table, the first several commands go out and then the burst exhausts the coordinator's own origination budget. The drag stops taking effect partway through, the remaining commands are never transmitted, and it surfaces as delivery errors in the log.

With a larger table they keep flowing, and the remaining risk is that individual routers drop some of them. Per the firmware survey in zigpy/ziggurat#55 most routers in a typical network run 40–56, so that is uncommon, and the capture in that PR found routers still relaying through a sustained burst.

Both end the same way for the user when they go wrong — the light stops following the wheel — but only the second lets the network try. Refusing early does save airtime, since the refused frames are never sent. What it does not do is protect neighbours' tables: a 15-entry router admits roughly nine per window and drops the rest whether the coordinator offered nine or fifty-eight, so a size mismatch costs airtime rather than peer table space. And a coordinator that goes quiet does not save even that cleanly, for the passive-acknowledgement reason below.

That is why a lower value does not read as the safer default here: for a coordinator it makes the failure certain rather than unlikely.

The conditional raised earlier in this thread — that it depends whether the routers in the network are extended — is the right test, and the survey in zigpy/ziggurat#55 is the answer to it: for networks built from the common router brands, they largely are.

Where that leaves the PR

Since the write is skipped on every current coordinator, nothing here changes behaviour as it stands, so closing seems reasonable. Two things worth keeping out of it either way.

The other half of the suggestion — documenting the override rather than only changing the default — seems worth doing independently of what happens to this PR, and it may already cover the concern here. A user-supplied ezsp_config entry is turned into a RuntimeConfig without minimum=True, so unlike the defaults it is written unconditionally and can lower a value as well as raise it. Anyone who does want the conservative behaviour can therefore already have it, without changing the default for everyone:

zha:
  zigpy_config:
    ezsp_config:
      CONFIG_BROADCAST_TABLE_SIZE: 15

which bellows will send as-is rather than treating it as a minimum. Lowering does take effect on the NCP; raising is rejected outright by the firmware rather than clamped, so on a ZBT-2 the real ceiling is its built-in 64 and a config line asking for 254 simply fails. That the stock coordinator firmware already ships 64 is probably the most useful thing for such a page to say.

Why lowering takes effect and raising cannot

The stack's broadcast logic reads the table size from a runtime variable rather than the compile-time constant — routing/util/broadcast.h defines BROADCAST_TABLE_SIZE as sli_zigbee_broadcast_table_size, which sl_zigbee_configuration.c only initialises from the constant. The NCP side resolves EZSP config ids through a pair of tables: configCodes, the supported ids in order, and configLocations, pointers to the variables they write. Position 6 of the first is 0x2b and entry 6 of the second is sli_zigbee_broadcast_table_size — so CONFIG_BROADCAST_TABLE_SIZE writes exactly the variable the stack reads. Those tables live in the prebuilt libncp-pro-library.a rather than in shipped source, but their relocations name the variables plainly. SiLabs' own host-side framework also sets the same config id, with a documented range of 15–254.

The same pairing puts 0x35 against sli_zigbee_new_broadcast_entry_threshold, which is what makes CONFIG_NEW_BROADCAST_ENTRY_THRESHOLD an independently settable knob rather than only a derived one.

Raising is the direction that cannot work, and the firmware is explicit about it: for these entries the handler compares the requested value against the current one and returns an error rather than storing, so a request above the compiled size fails instead of being clamped. That fits the memory layout — the table is a statically allocated array sized by the compile-time constant, with nothing behind a larger runtime value. One consequence worth knowing if this gets documented: within a single NCP session the value can only ever go down, so recovering a lowered value takes an adapter reset, after which the firmware default applies again.

And if a broadcast table entry is ever added for some other reason, the getConfigurationValue fall-through noted above is worth a guard.

Background, alternatives and caveats

None of the above depends on these — they are the working behind it, the alternative I considered and rejected, and the places where the concern in this PR does hold.

On "Zigbee PRO must have it set to 15"

SiLabs' own comment next to the default says otherwise:

/* The minimum broadcast table size per the Zigbee Pro spec is 9. */
#define SL_ZIGBEE_DEFAULT_BROADCAST_TABLE_SIZE 15

The configurable range is 15–254. R23.2 itself does not specify a broadcast table size; nwkNetworkBroadcastDeliveryTime is listed with a default of "Defined in stack profile", and the familiar 9 s figure comes from the Zigbee PRO stack profile document rather than the base spec.

The same header goes further, and is worth reading alongside the stack comparison in zigpy/ziggurat#43. SiLabs describe 15 not as a spec value but as their own compensation for departing from one:

The ZigBee Pro profile document 074855r05 states a value of 9 seconds for the entry timeout, which is too short. We double the timeout and the table size in order to get the same bandwidth as the spec.

So 15 is the number that exists because their entry timeout departs from the spec — a compensation for holding entries 15–20 s instead of 9 s, sized to land at the same bandwidth. Reading it as a floor the spec imposes gets it backwards.

The burst clamp layered on top has no spec counterpart at all: the same header describes it as approximating "the behavior of the pre-z3.1 stack", which is backwards compatibility rather than conformance. R23.2 section 3.6.6 asks only that a device track each broadcast, drop new ones when the table is full, and expire entries after the delivery time — it sets no table size, no rate, and no burst limit. That is the point the comparison in zigpy/ziggurat#43 makes across stacks, and it puts SiLabs at 15 at the conservative end of the group rather than at the reference point.

Why not drop minimum=True and clamp the coordinator down to 15 instead?

This is the strongest form of the argument, and SiLabs' own warning does point at it:

Additionally, this value must be universal for all devices in the network. Otherwise, a single router can overwhelm all its neighbors with more broadcasts than they can support.

Read literally, a coordinator at 64 among neighbours at 15–50 is exactly that asymmetry. Mechanically it would also work, unlike raising: the firmware accepts a lower value and rejects a higher one, and the table is allocated at the compiled size so a smaller runtime size just leaves entries unused.

It would still be the first entry of its kind in bellows. All eight existing minimum=True entries in bellows/ezsp/config.py are capacities — multicast, address, key and source route tables, trust center cache, end device children, supported networks. Everything written unconditionally is a behavioural or protocol setting. The one capacity written unconditionally is CONFIG_PACKET_BUFFER_COUNT, set to 0xFF, so it cannot lower anything either. Nothing in the file currently shrinks a table below what the firmware chose.

There is also a second-order effect that runs against the intuition that a smaller table is the polite choice. Passive acknowledgement means a router keeps retransmitting a broadcast, up to nwkMaxBroadcastRetries times, at any neighbour it has not heard relay it (R23.2 section 3.6.6). A coordinator that drops a broadcast because its table is full does not go quietly — its neighbours retransmit at it. Clamping therefore buys less airtime than it appears to, and may cost some.

The one thing a lower origination limit would not address

Worth conceding, since it is the strongest technical form of the concern here.

The threshold governs what the coordinator originates. A coordinator with a 64-entry table can still relay a lot of other devices' broadcast traffic, and if it sits between two parts of a mesh it can forward more distinct concurrent transactions into one side than 15-entry routers there can track. Lowering the origination limit would not change that.

There is a caveat on that caveat: routing/util/broadcast.h describes the same threshold as gating any new entry added within the last 10–15 s rather than originated ones specifically, in which case lowering it would throttle relaying too. The two SDK descriptions disagree and the implementation is in the closed libraries, so I cannot say which governs.

Either way the effect looks bounded. A router whose table is full drops the frame and neither relays it nor passes it up (R23.2 section 3.6.6), so it cannot be pushed past its own capacity — what changes is which broadcasts occupy its slots, plus the airtime of the extra relays. Where that does bite is the original worry about floods breaking network operations: link status is a one-hop broadcast that is never relayed and so is unaffected, but Device_annce (0xFFFD) and Mgmt_Permit_Joining_req (0xFFFC) are ordinary tracked broadcasts, so saturated peer tables really can make a newly joined device invisible or permit-join unreliable. It also requires the coordinator to be the only path between two regions; in a reasonably meshed network those transactions arrive from other routers anyway. And on a typical ZHA network most broadcasts originate at the coordinator in the first place, so there is not much third-party traffic to amplify.

That reads as a reason to leave the shipped configuration alone rather than to pick a different number for it.

On the IEEE 802.15.4 broadcast limit discussed in the linked threads

This is not this PR's stated rationale, but it has come up in zigpy/ziggurat#43 and zigpy/ziggurat#55, and it matters here in one narrow way: if there were a broadcast limiter below the Zigbee layer, a larger broadcast table would not actually deliver the extra headroom described above. As far as I can tell there is not, so it does.

802.15.4 does have broadcasts — destination short address 0xFFFF, unacknowledged, and beacon requests are pure MAC broadcasts with no Zigbee layer involved at all. What it does not define is any relaying of one. R23.2 section 3.6.6 places the relay decision, the duplicate check and the drop-when-full rule in the network layer's Broadcast Transaction Table; the MAC's part is issuing MCPS-DATA.request to 0xFFFF with acknowledgement disabled, repeated up to nwkMaxBroadcastRetries times by the NWK layer. Radius and destination-type checks gate relaying alongside the BTT. So all of the limiting lives in the stack, and on EmberZNet it is visibly in routing/util/broadcast.h — the table, the aging buckets and the burst clamp discussed above.

Two caveats on how strongly to put that. 802.15.4 standardises no broadcast rate limiting, which is not the same as proving that no MAC implementation anywhere could throttle broadcast frames — there is simply no sign of one here. And there are real MAC-level effects under load that can look like a broadcast limiter without being one: CSMA/CCA channel access failures, the absence of MAC acknowledgement and retries on broadcast frames, and finite firmware queues. Those hit all traffic rather than broadcasts specifically, and the radio counters distinguish them — COUNTER_BROADCAST_TABLE_FULL rising points at the table, COUNTER_PHY_CCA_FAIL_COUNT at airtime.

A useful cross-check, if the closed EmberZNet libraries are not satisfying: OpenThread is an unrelated stack over the same radio standard and is fully open source. Its multicast forwarding control is also at the network layer, in MPL (src/core/net/ip6_mpl.cpp), with its own duplicate-detection table of 35 entries at 5 s. Two independent stacks over 802.15.4 both had to build this above the MAC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants